#define VERBOSEDEBUG
#define VERBOSE_DIFFERENCES

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Serialization;

namespace SerializationUtils
{
	/// <summary>
	/// Deserializes objects in a version tolerant manner. For the purposes of this
	/// implementation this means that missing fields or new fields do not break the
	/// deserialization of the stream.
	/// 
	/// This also handles changes to names of fields. This will allow you to map new
	/// field names to field names that were used when the data was originally
	/// serialized. 
	/// 
	/// New fields are initialized to zero (null). You can implement
	/// IDeserializationCallback in your individual classes if you want the ability to
	/// initialize fields after deserialization has been completed.
	/// If you want total control over the serialization/deserialization process, then
	/// do not use this class. Instead, implement the ISerializable interface and
	/// perform your own custom serialization logic.
	/// 
	/// Currently, changes to the data type is not handled - use a new field name for that.
	/// 
	/// <remarks>
	/// The mechanism used is the ISerializationSurrogate interface. This allows an
	/// external class to handle the serialization/deserialization of a class.
	/// At this time there is no need to use this when serializing the data. However,
	/// when deserializing the data this class can be used to map on older version of an
	/// object to a newer version of the object.
	/// 
	/// To use this, follow these steps (see sample code at end of file)
	/// 1. Construct the desired formatter
	/// 2. Construct an instance of this class.
	/// 3. Add to this instance all the types that it must handle. This list MUST be
	/// inclusive of all classes that must be handled. This defines all classes for
	/// which missing fields will be ingnored.
	/// 4. Add to this instance a listing of all field names that need to be mapped from
	/// older field names.  This is optional. 
	/// 5. Tell the formatter to use this class as the surrogate. You do this my setting:
	///     formatter.SurrogateSelector = thisClassInstance.Surrogate;
	/// 6. Deserialize your object graph.
	/// </remarks>
	/// </summary>
	public class VersionTolerantSerializer : ISerializationSurrogate
	{

		// =====================================
		#region ctor, dtor, cleanup

		/// <summary>
		/// A default object
		/// </summary>
		public VersionTolerantSerializer()
		{
			_selector = new SurrogateSelector();
			_mapOldNameToNewName = new Dictionary<string, Dictionary<string, string>>();
			_mapTypeToSerializationInfo = new Dictionary<string, Dictionary<string, SerializationEntry>>();
		}

		#endregion ctor, dtor, cleanup
		// =====================================

		// =====================================
		#region fields, properties, types

		private bool _quiet = false;
		/// <summary>
		/// If false (not quiet) then this traces a message each time a value could not be
		/// deserialized from a data stream. If true (quiet mode) then no message is traced. The
		/// default is false (noisy mode).
		/// </summary>
		public bool Quiet
		{
			get { return _quiet; }
			set { _quiet = value; }
		}

		private SurrogateSelector _selector = null;
		/// <summary>
		/// The Surrogate selector that this class uses.
		/// You must set your serialization formatter to this instance in order for this
		/// to function correctly.
		/// </summary>
		public SurrogateSelector Selector
		{
			get { return _selector; }
		}

		/// <summary>
		/// The outer dictionary maps types to collections of Dictionaries.
		/// Each inner dictionary maps a field name to a SerializationEntry object. 
		/// The mapping looks like: 
		/// Class/type --> field name --> SerializationEntry
		/// </summary>
		private Dictionary<string, Dictionary<string,SerializationEntry>> _mapTypeToSerializationInfo;
		private Dictionary<string, Dictionary<string, string>> _mapOldNameToNewName;


		#endregion fields, properties, types
		// =====================================

		// =====================================
		#region methods

		/// <summary>
		/// Adds a type to the list of types that this surrogate will handle. This MUST
		/// be invoked for each type that will be managed. For each type handled
		/// missing fields will be ignored. This means that fields that were present in
		/// the original serialized data but which are not present in the current
		/// implementation of the class will be skipped so that deserialization
		/// formatter will not throw an exception.
		/// </summary>
		/// <param name="t"></param>
		public void AddType( Type t )
		{
			_selector.AddSurrogate( t, new StreamingContext( StreamingContextStates.All ), this );
		}


		/// <summary>
		/// Maps an old field name to a new field name. The field data type must remain
		/// the same. The names are case-sensitive.
		/// You must still call AddType for each type that you want this to handle.
		/// </summary>
		/// <param name="t">The type</param>
		/// <param name="oldFieldName">the old field name</param>
		/// <param name="newFieldName">The new field name</param>
		public void AddMapping( Type t, string oldFieldName, string newFieldName )
		{
			Dictionary<string, string> l;
			if ( _mapOldNameToNewName.ContainsKey( t.FullName ) )
			{
				l = _mapOldNameToNewName[ t.FullName ];
			}
			else
			{
				l = new Dictionary<string, string>();
				_mapOldNameToNewName.Add( t.FullName, l );
			}
			// map expected new name to the existing (old) name
			l.Add( newFieldName, oldFieldName ); 
		}


		/// <summary>
		/// Builds a list of field names and types available to be deserialized from the serialization info.
		/// Returns a dictionary of field/property names mapped to SerializationEntry objects. 
		/// This is the list of field names/types that were present when the data was originally serialized.
		/// </summary>
		/// <param name="fullTypeName"></param>
		/// <param name="context"></param>
		/// <returns></returns>
		private Dictionary<string, SerializationEntry> MapTypeNameToSerializedEntries( SerializationInfo info )
		{
			if ( _mapTypeToSerializationInfo.ContainsKey( info.FullTypeName ) )
				return _mapTypeToSerializationInfo[ info.FullTypeName ];

			Dictionary<string, SerializationEntry> l = new Dictionary<string, SerializationEntry>();
			_mapTypeToSerializationInfo.Add( info.FullTypeName, l );

			SerializationInfoEnumerator i = info.GetEnumerator();
			while ( i.MoveNext() )
			{
				SerializationEntry se = i.Current;
				l.Add( se.Name, se );
			}

			return l;
		}	//	MapTypeNameToSerializedEntries


		/// <summary>
		/// If a set of mappings have been defined this returns a Dictionary of mapped
		/// names. If there are no mappings then this returns a null.
		/// </summary>
		/// <param name="info"></param>
		/// <returns></returns>
		private Dictionary<string, string> GetFieldNameMappings( SerializationInfo info )
		{
			if ( _mapOldNameToNewName.ContainsKey( info.FullTypeName ) )
			{
				return _mapOldNameToNewName[ info.FullTypeName ];
			}
			return null;
		}

		// =====================================
		#region ISerializationSurrogate Members

		/// <summary>
		/// Gets the data from the object and adds it to the SerializationInfo object.
		/// This really ought not to be used since the standard serialization routines
		/// are sufficient.
		/// If this method is getting called then you are using this during
		/// serialization....DO NOT DO THIS (but if you do it should work).
		/// </summary>
		/// <param name="obj"></param>
		/// <param name="info"></param>
		/// <param name="context"></param>
		public void GetObjectData( object obj, SerializationInfo info, StreamingContext context )
		{
			MemberInfo[] members = FormatterServices.GetSerializableMembers( obj.GetType(), context );
			object[] data = FormatterServices.GetObjectData( obj, members );
			for ( int x=0; x < data.Length; x++ )
			{
				info.AddValue( members[ x ].Name, data[ x ] );
			}
		}

		/// <summary>
		/// Takes the data in the SerializationInfo object and uses it to populate the object.
		/// This is the workhorse of the class.
		/// </summary>
		/// <param name="obj"></param>
		/// <param name="info"></param>
		/// <param name="context"></param>
		/// <param name="selector"></param>
		/// <returns></returns>
		public object SetObjectData( object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector )
		{
			Dictionary<string, SerializationEntry> entries = MapTypeNameToSerializedEntries( info );
			Dictionary<string, string> mappings = GetFieldNameMappings( info );
			Type t = obj.GetType();
			MemberInfo[] members = FormatterServices.GetSerializableMembers( t, context );
#if VERBOSEDEBUG
			Trace.WriteLine( string.Format( "Deserializing type={0}.",t.FullName) );
			//foreach ( string key in entries.Keys )
			//{
			//    Trace.WriteLine( string.Format( "  key={0}, NameOfType={1}", key, entries[ key ].ObjectType.Name) );
			//}
#endif
			foreach ( MemberInfo mi in members )
			{
				FieldInfo fi = (FieldInfo)mi;
				// name of the field as it exists right now
				string currentFieldName = mi.Name;
				// check for mapping from current to an older name
				if ( mappings != null )
				{
					// is current name mapped from an older name
					if ( mappings.ContainsKey( currentFieldName ) )
					{ 
						// get name as it was when the data was serialized
						string oldFieldName = mappings[ currentFieldName ];
						// only replace if name exists, otherwise try current name
						if ( entries.ContainsKey( oldFieldName ) ) 
							currentFieldName = oldFieldName;
					}
				}
				// check if target name exists in serialization info
				if ( !entries.ContainsKey( currentFieldName ) )
				{
					if ( !Quiet )
						Trace.WriteLine( string.Format( "Unable to deserialize a value for type.name={0}.{1}. The current field name is not present in the serialized data and a mapping from the current name to an old name was not supplied.",
							info.FullTypeName, mi.Name ) );
					continue; // name not present in serialized data, so dont try to deserialize it.
				}
				SerializationEntry se = entries[ currentFieldName ];
				object val = se.Value;
				// check if type has changed
				if ( se.ObjectType != fi.FieldType )
				{
#if VERBOSE_DIFFERENCES
					Trace.WriteLine( string.Format( "Types have changed for field '{0}'. OldTypeName={1}, newTypeName={2}, val={3};",
						currentFieldName, se.ObjectType, fi.FieldType, val ) );
#endif
					val = ChangeType( val, se.ObjectType, fi.FieldType );
				}
#if VERBOSEDEBUG
				Trace.WriteLine( string.Format( "  fieldName={0}, type={1}, value={2}", currentFieldName,fi.FieldType,val ) );
#endif
				fi.SetValue( obj, val );
			}
			return obj;
		}	//	SetObjectData

		#endregion	ISerializationSurrogate Members
		// =====================================

		/// <summary>
		/// This performs some limited type conversion. If a conversion is not
		/// implemented this returns the original value. If a conversion fails and
		/// throws an exception the exception is propagated upwards.
		/// </summary>
		/// <param name="initialValue"></param>
		/// <param name="newType"></param>
		/// <returns></returns>
		private object ChangeType( object initialValue, Type oldType, Type newType )
		{
			if ( newType.IsSubclassOf( typeof( Enum ) ) )
			{
				if ( oldType == typeof( string ) )
				{
					return Enum.Parse( newType, (string)initialValue, true );
				}
			}
			return Convert.ChangeType( initialValue, newType );
		}


		#endregion methods
		// =====================================



#if SAMPLECODE

		/// <summary>
		/// This shows how to use this class. 
		/// </summary>
		static void DemoDeserialize()
		{
			try
			{
				// 0. modify the classes (not shown) to change names or delete fields
				// 
				// 1. Construct the desired formatter (soap, binary, etc)
				IFormatter formatter = new SoapFormatter();

				// 2. Construct a version tolerant deserializer
				VersionTolerantSerializer vts = new VersionTolerantSerializer();

				// 3. Tell the serializer which types (classes) to handle
				vts.AddType( typeof( MyNameSpace.MyClass ) );
				vts.AddType( typeof( MyNameSpace.MyClass.MyOtherClass ) ); // nested class
				vts.AddType( typeof( MyNameSpace.SomeOtherClass ) ); // another class
				// add as many types as needed

				// 4. tell it which mappings to perform
				vts.AddMapping( typeof( MyNameSpace.MyClass ), "AFieldName", "SomeOtherName" );
				vts.AddMapping( typeof( MyNameSpace.MyClass.MyOtherClass ), "AFieldInANestedClass", "IPreferredSomeOtherName" );

				// 5. Have the formatter use our surrogate selector
				formatter.SurrogateSelector = vts.Selector;

				// 6. Read in previously serialized data
				using ( FileStream stream = new FileStream( "MySerializedData.xml", FileMode.Open ) )
				{
					MyNameSpace.MyClass c = (MyNameSpace.MyClass)formatter.Deserialize( stream );
					// use the class
				}
			}
			catch ( Exception ex )
			{
				Trace.WriteLine( ex.ToString() );
			}
		}
#endif


	}	//	class VersionTolerantSerializer
}
